package com.dyz.wsw.security.filter;

import com.dyz.wsw.common.consts.data.AccountConst;
import com.dyz.wsw.common.consts.web.HttpConst;
import com.dyz.wsw.common.consts.web.JwtConst;
import com.dyz.wsw.common.consts.web.SecurityConst;
import com.dyz.wsw.common.enumerator.ServiceCode;
import com.dyz.wsw.common.pojo.auth.Principal;
import com.dyz.wsw.common.utils.converter.Json;
import com.dyz.wsw.common.utils.security.JWTs;
import com.dyz.wsw.common.utils.security.MD5;
import com.dyz.wsw.common.web.JsonResult;
import com.dyz.wsw.security.config.WswSecurityJWTProperties;
import com.dyz.wsw.security.repository.WswSecurityCacheRepository;
import com.dyz.wsw.security.utils.IPUtils;
import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 万事屋认证校验过滤器
 * 主要检测请求头中有没对应的id，如果有则传入流中供后续使用
 * @author dyz
 * @date 2025/2/15 19:11
 */
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class WswSecurityJwtFilter implements WebFilter, SecurityConst, HttpConst, JwtConst, AccountConst {


    private final WswSecurityJWTProperties properties;

    private final WswSecurityCacheRepository wswSecurityCacheRepository;


    public WswSecurityJwtFilter(WswSecurityJWTProperties properties,WswSecurityCacheRepository wswSecurityCacheRepository) {
        log.debug("创建万事屋安全认证过滤器对象: WswSecurityJwtFilter");
        this.properties = properties;
        this.wswSecurityCacheRepository = wswSecurityCacheRepository;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        String ip = IPUtils.getIp(request);
        log.debug("ip: {},请求路径: {}",ip,path);
        //1.放行允许的请求，无需jwt校验
        if(release(properties.getPermitUri(),path)){
            return chain.filter(exchange);
        }
        //2.简单校验jwt，如果失败，直接快速失败返回401
        HttpHeaders headers = request.getHeaders();
        log.debug("headers: {}",headers);

        String token = headers.getFirst(HEADER_AUTHORIZATION);
        log.debug("token: {}",token);

        ServerHttpResponse response = exchange.getResponse();
        if(!StringUtils.hasText(token)||token.length()<JWT_MIN_LENGTH){
            log.warn("ip: {},本次请求没有携带有效的jwt,拒绝服务!,请求路径: {}",ip,path);

            return reject(JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED,"未认证"),response);
        }

        //3.进一步校验jwt
        Claims claims = parseToken(token);
        if(claims==null){
            return reject(JsonResult.fail(ServiceCode.ERROR_JWT_FORMAT,"非法令牌访问"),response);
        }

        //4.校验jwt是否过了最大有效期
        Long iat = claims.get(CLAIM_JWT_IAT,Long.class);

        if(System.currentTimeMillis()-iat>properties.getJwtMaxExpireTime()*60*1000){
            log.warn("ip: {},jwt已经过最长有效期，请重新登录!",ip);
            return reject(JsonResult.fail(ServiceCode.ERROR_JWT_EXPIRED,"令牌过期或失效,请重新登录"),response);
        }


        //4.校验jwt是否存在盗用
        Long userId = claims.get(CLAIM_USER_ID, Long.class);
        String username = claims.get(CLAIM_USER_NAME, String.class);
        String jti = claims.get(CLAIM_JWT_JTI,String.class);

        if(!checkSteal(ip,jti)){
            log.warn("ip: {},jwt存在盗用风险,拒绝服务!",ip);
            return reject(JsonResult.fail(ServiceCode.ERROR_JWT_SIGNATURE,"令牌存在盗用风险"),response);
        }




        //5.从缓存中读取用户登录信息
        log.debug("开始检查缓存中用户的状态...");

        return wswSecurityCacheRepository.getUserState(userId)
                .switchIfEmpty(Mono.defer(()->{
                    log.warn("缓存中查无此条数据，请重新登录认证！");
                    return reject(JsonResult.fail(ServiceCode.ERROR_JWT_EXPIRED,"令牌过期或失效，请重新登录！"),response)
                            .then(Mono.defer(Mono::empty));
                }))
                .flatMap(userCachePO -> {
                    Integer enable = userCachePO.getEnable();
                    if (!enable.equals(ENABLE_STATE_ON)) {
                        return wswSecurityCacheRepository
                                .deleteUserState(userId)
                                .then(Mono.defer(()->{
                                    String message = "您的账号【"+username+"】已经被禁用,将强制下线";
                                    log.warn(message);
                                    return reject(JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED_DISABLED, message),response);
                                }));
                    }
                    //续期缓存中的用户登录信息
                    //6.以上都没问题，那么就放行
                    //然后将解析出来的数据交给后续组件处理
                    return wswSecurityCacheRepository.renewal(userId)
                            .then(Mono.defer(()->{
                                String authoritiesJsonString = userCachePO.getAuthoritiesJsonString();
                                log.debug("从Redis中读取当前用户的权限列表 = {}", authoritiesJsonString);
                                List<String> permissionList = Json.toStringList(authoritiesJsonString);
                                Principal principal = new Principal();
                                principal.setId(userId);
                                principal.setUsername(username);
                                log.debug("验证jwt完毕,即将放行!");
                                return chain.filter(exchange)
                                        .contextWrite(Context.of(
                                                PRINCIPAL,principal,
                                                PERMISSIONS,permissionList
                                        ));
                            }));
                });
    }


    /**
     * 检查jwt盗用问题
     * 目前特征码就采用ip+user-agent进行md5计算的值为依赖
     * @param ip 计算特征码所需ip
     * @param jti 客户端携带的jwt中的特征码
     * @return 检查盗用结果
     */
    private boolean checkSteal(String ip,String jti){
        String thisJti = MD5.encrypt(ip);
        log.debug("本次计算特征码: {}",thisJti);
        return jti.equals(thisJti);
    }


    /**
     * 进一步解析token
     * @param token 被解析的token
     * @return 解析结果,null为非法
     */
    private Claims parseToken(String token){
        token = token.substring(properties.getJwtPrefix().length()).trim();
        log.debug("jwt: {}",token);
        Claims claims;
        try {
            claims = JWTs.parse(token, properties.getPrivateKey());
        }catch (Exception e){
            log.warn("非法jwt: {}",token);
            return null;
        }


        log.debug("jwt解析结果: {}",claims);
        return claims;
    }


    /**
     * 快速失败，拒绝服务
     * @param result 返回的数据
     * @param response 本次响应对象
     * @return 响应结果
     */
    private Mono<Void> reject(JsonResult result, ServerHttpResponse response) {
        String json = new Gson().toJson(result);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return response.writeWith(Mono.just(response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8))));
    }


    /**
     * 放行请求
     * @param permitUri 放行路径
     * @param path 请求路径
     * @return 放行结果
     */
    private boolean release(List<String> permitUri, String path){
        if (permitUri==null||permitUri.isEmpty()) return false;
        boolean flag = false;
        for (String uri : permitUri) {
            if (path.equals(uri)||subStar(uri,path)) {
                flag = true;
                break;
            }
        }
        return flag;
    }


    /**
     * 看路径中有没有*,有*按通配符进行处理
     * @param uri 放行路径
     * @param path 本次请求路径
     * @return 放行结果2
     */
    private boolean subStar(String uri,String path){
        uri = uri.trim();
        if(!uri.contains("*")){
            return false;
        }
        int starIndex = uri.indexOf('*');
        uri = uri.substring(0, starIndex);
        return path.startsWith(uri);
    }
}
